iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
自我挑戰組

攜手 AI 從零開始打造一款 Flutter 應用程式系列 第 8

Day 8: 從「能看」到「能用」- 處理使用者輸入與 Form 表單

  • 分享至 

  • xImage
  •  

前言

大家好!「省錢拍拍」App 經過第七天的形象改造,目前為止,它仍然是一個只能「看」的 App,使用者無法輸入自己的消費數據。

今天,我們將跨出從「靜態」到「互動」最關鍵的一步。我們將:

  1. 建立一個全新的「新增消費」頁面。
  2. 學習如何在頁面之間進行導航 (Navigation)。
  3. 使用 TextField 來接收使用者輸入。
  4. 利用 FormTextFormField 來對輸入的資料進行組合與驗證。

準備好讓我們的 App 真正「活」起來了嗎?讓我們開始吧!

Step 1: 建立新頁面並實現導航

要新增消費,我們首先需要一個入口。最符合直覺的設計,就是在主畫面的右下角放置一個懸浮按鈕 (FloatingActionButton)。

  1. 在主畫面加入按鈕
    打開 lib/main.dart,找到 HomePageScaffold,在裡面加入 floatingActionButton 屬性。
// lib/main.dart -> HomePage -> build
// ...
return Scaffold(
  appBar: AppBar( /* ... */ ),
  // 在 Scaffold 加入 FAB
  floatingActionButton: FloatingActionButton(
    onPressed: () {
      // 點擊後要執行的導航動作
    },
    child: const Icon(Icons.add),
  ),
  body: Column( /* ... */ ),
);
  1. 建立新頁面的檔案
    為了保持專案結構清晰,我們為新頁面建立一個獨立的檔案。在 lib 資料夾上按右鍵,選擇 New File,將其命名為 add_transaction_page.dart

在檔案中,先放入一個最基本的頁面結構:

// lib/add_transaction_page.dart
import 'package:flutter/material.dart';

class AddTransactionPage extends StatelessWidget {
  const AddTransactionPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新增一筆消費'),
      ),
      body: const Center(
        child: Text('這裡是新增消費的表單'),
      ),
    );
  }
}
  1. 串連導航
    回到 lib/main.dart,我們使用 Navigator.push 這個方法來實現頁面跳轉。
// lib/main.dart (記得在頂部 import 新檔案)
import 'package:snapsaver/add_transaction_page.dart'; // 根據你的專案結構調整路徑

// ...
// lib/main.dart -> HomePage -> floatingActionButton
floatingActionButton: FloatingActionButton(
  onPressed: () {
    // Navigator.push 會將一個新的 "Route" 推送到導航堆疊上
    Navigator.push(
      context,
      // MaterialPageRoute 是一種標準的頁面切換動畫效果
      MaterialPageRoute(builder: (context) => const AddTransactionPage()),
    );
  },
  child: const Icon(Icons.add),
),
// ...

重啟你的 App,點擊右下角的 + 按鈕,你應該能成功跳轉到新的「新增消費」頁面了!Flutter 會自動在 AppBar 左側加上返回按鈕。

Step 2: TextField 與 TextEditingController

TextField 是最基礎的文字輸入框。但要有效地使用它,我們需要一個「控制器」——TextEditingController,來讀取、監聽或修改輸入框內的文字。

為了管理 TextEditingController 的生命週期,我們需要將 AddTransactionPageStatelessWidget 轉換為 StatefulWidget

// lib/add_transaction_page.dart

// 轉換為 StatefulWidget
class AddTransactionPage extends StatefulWidget {
  const AddTransactionPage({super.key});

  @override
  State<AddTransactionPage> createState() => _AddTransactionPageState();
}

class _AddTransactionPageState extends State<AddTransactionPage> {
  // 1. 宣告 Controller
  final TextEditingController _titleController = TextEditingController();

  // 2. 在 dispose 方法中釋放 Controller,防止內存洩漏
  @override
  void dispose() {
    _titleController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新增一筆消費'),
      ),
      body: ListView( // 使用 ListView 讓內容超出時可以滾動
        padding: const EdgeInsets.all(16.0),
        children: [
          TextField(
            controller: _titleController, // 3. 綁定 Controller
            decoration: const InputDecoration(
              labelText: '品項名稱', // 標籤文字
              hintText: '例如:早餐', // 提示文字
              border: OutlineInputBorder(), // 加上外框線
            ),
          ),
        ],
      ),
    );
  }
}

Step 3: Form 與 TextFormField 的威力

當我們有多個輸入框時,逐一管理和驗證會變得很麻煩。Form Widget 就是為此而生。它搭配 TextFormField(一個內建驗證功能的 TextField),可以輕鬆實現整份表單的統一驗證與提交。

  1. GlobalKey: 我們需要一個 GlobalKey<FormState> 來作為 Form 的唯一標識,以便我們在外部可以操作這個 Form
  2. Form Widget: 用 Form 包裹住所有的 TextFormField
  3. TextFormField: 將 TextField 替換為 TextFormField,並為其提供 validator 函式。
  4. 驗證觸發: 在提交按鈕的 onPressed 中,使用 _formKey.currentState!.validate() 來觸發所有欄位的驗證。

Step 4: 打造完整的「新增消費」表單

將以上概念整合起來,打造一個包含品項、金額,並具備驗證功能的完整表單。

// lib/add_transaction_page.dart (完整範例)
import 'package:flutter/material.dart';

class AddTransactionPage extends StatefulWidget {
  const AddTransactionPage({super.key});
  @override
  State<AddTransactionPage> createState() => _AddTransactionPageState();
}

class _AddTransactionPageState extends State<AddTransactionPage> {
  // 1. 建立 GlobalKey
  final _formKey = GlobalKey<FormState>();

  // 2. 建立各個欄位的 Controller
  final _titleController = TextEditingController();
  final _amountController = TextEditingController();

  @override
  void dispose() {
    _titleController.dispose();
    _amountController.dispose();
    super.dispose();
  }

  void _submitForm() {
    // 3. 觸發驗證
    if (_formKey.currentState!.validate()) {
      // 如果驗證通過
      final title = _titleController.text;
      final amount = double.tryParse(_amountController.text) ?? 0.0;

      print('品項: $title, 金額: $amount');
      // 在這裡,我們未來會將資料傳回主頁面
      
      // 驗證通過後,返回上一頁
      Navigator.pop(context);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('新增一筆消費'),
      ),
      body: Form( // 4. 用 Form 包裹
        key: _formKey, // 5. 綁定 Key
        child: ListView(
          padding: const EdgeInsets.all(16.0),
          children: [
            // 6. 將 TextField 改為 TextFormField
            TextFormField(
              controller: _titleController,
              decoration: const InputDecoration(labelText: '品項名稱'),
              // 7. 加入驗證邏輯
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '請輸入品項名稱';
                }
                return null; // 回傳 null 代表驗證通過
              },
            ),
            const SizedBox(height: 16),
            TextFormField(
              controller: _amountController,
              decoration: const InputDecoration(labelText: '金額'),
              keyboardType: TextInputType.number, // 設定鍵盤為數字鍵盤
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return '請輸入金額';
                }
                if (double.tryParse(value) == null) {
                  return '請輸入有效的數字';
                }
                return null;
              },
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: _submitForm, // 8. 綁定提交函式
              child: const Text('儲存'),
            ),
          ],
        ),
      ),
    );
  }
}

現在,當你留白或輸入無效數字並按下「儲存」時,TextFormField 下方就會自動顯示我們定義的錯誤訊息!

今日結語

今天完成了從 0 到 1 的互動性突破!我們學會了:

  • 如何建立新頁面,並使用 Navigator.push 在頁面間導航。
  • 使用 TextEditingController 來管理 TextField 的內容。
  • 整合 Form, TextFormField, GlobalKey 來打造一個具備完整驗證邏輯的表單。

按下儲存後,資料只是被印在終端機裡,主畫面的列表並沒有更新。明天,我們將著手處理 Flutter 開發中最核心、也最有趣的話題之一:狀態管理 (State Management)。我們將學習如何將新頁面的資料,安全地傳遞回主頁面,並真正地更新我們的列表 UI!


上一篇
Day 7: App 的整體造型師 - 使用 ThemeData 打造專屬風格
系列文
攜手 AI 從零開始打造一款 Flutter 應用程式8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言